<?php
/**
 * set up playlists for the jPlayer application
 *
 * This class maintains a set of playlists for the jPlayer Javascript Media
 * Player. It expects a directory containing one directory for each playlist (or
 * genre as it is termed in this class).
 * In those directories is expected a set of tagged mp3 files. (other formats
 * are possible, but that has not been tested in this version). The class
 * checks for the presence of an XML file containing the playlist data, if the
 * file is not present, one is created. This XML file is built using the ID tags
 * of the mp3 files. The playlist order can be set by manually editing the XML
 * file. The public methods of the class give a list of playlists and a
 * JSON-encoded string of the playlist itself for use by the jPlayer application.
 *
 * PHP version 5.2
 *
 * @author     Roland Barker <webdesign@xnau.com>
 * @copyright  2011 xnau webdesign Roland Barker
 * @license    GPL2
 * @version    0.3
 * @depends    GetID3 Class, SimpleXML
 *
 * the XML format:
 * <playlist genre="genre tag" path="path" count="number of items in the playlist" >
 *   <song>
 *    <title>Song Title</title>
 *    <artist>Artist Name</artist>
 *    <filename>song.mp3</filename>
 *    <fileformat>mp3</fileformat>
 *   </song>
 *   ... and so on for each song
 * </playlist>
 * song order is simply the order of entries in the XML file
 *
 */
class jP_Playlists {

  // this is the name to use for the XML playlists in each genre directory
  const PLAYLIST_FILE = 'playlist.xml';

  // the master directory for playlist directories, relative to web root
  private $audio_dir;
  
  // path to audio for player
  private $audio_path;
  
  // an array of genre directories
  private $genres = array();
  
  /*
   * all the playlists in a 3-dimensional array:
   * [playlists]--
   *             [genres]--
   *                     [songs]--
   *                           [song data]
   */
  private $playlists;

  public function __construct( $directory = '' ) {

    if ( ! empty( $directory ) && is_dir($directory) ) $this->audio_dir = $directory;
    
    else {
      error_log( __CLASS__.' could not instantiate with directory: '.$directory );
      die( 'invalid directory: '.$directory );
    }

    // set the path to the audio for the player
    $this->audio_path = $this->_get_root_URL().$this->audio_dir;

    // collect the set of genre directories
    $this->_set_genres();

    // build the array of playlists
    $this->_collect_playlists();
    
    // error_log( __METHOD__.' playlists:'.print_r( $this->playlists, true ));

  }

  /******************
   * PUBLIC METHODS
   */

  /**
   * prints a JS object for one genre
   */
  public function print_genre_object( $genre ) {
    
    return json_encode( $this->playlists[ $genre ] );
    
  }
  
  /**
   * get a list of the genres
   */
  public function get_genres() {
    
    return $this->genres;
    
  }

  /******************
   * GENRE METHODS
   */

  /**
   * collects the genre directory paths into the $genres property
   *
   * @return NULL
   */
  private function _set_genres() {
    
    foreach( scandir( $this->audio_dir ) as $item ) {
    
      // check for valid directories
      if ( false !== $this->_get_directory_name( $item ) ) {
        
        $this->genres[ $item ] = $this->_make_title( $item );
        
      }
      
    }
    
  }

  /*****************
   * PLAYLIST METHODS
   */

  /**
   * sets up the master array of playlists
   *
   * @return null
   */
  private function _collect_playlists() {
    
    foreach( $this->genres as $genre => $genre_title ) {
      
      $playlist = $this->_get_playlist( $genre );

      // if the XML parsing went OK, the playlist is added
      if ( false !== $playlist ) $this->playlists[ $genre ] = $playlist;
      
      // remove it from the list of genres if it has no media files
      else unset( $this->genres[ $genre ] );
      
    }
    
  }

  /**
   * gets a playlist from an XML file
   *
   * @param string $genre the path of the genre directory
   * @return array of song data or false
   */
  private function _get_playlist( $genre ) {
    
      $xml_file = $this->audio_dir.DIRECTORY_SEPARATOR.$genre.DIRECTORY_SEPARATOR.self::PLAYLIST_FILE;
    
      // if the XML doesn't exist, make it
      if ( ! is_file( $xml_file ) ) {
      
        if ( false === $this->_make_xml( $genre, $xml_file ) ) return false;
        
      }
      
      return $this->_parse_xml( $xml_file );
  }

  /******************
   * XML METHODS
   */

  /**
   * parses an xml file into an array
   *
   * @param string $file path to XML file
   * @return array of song data for all songs in the genre directory
   */
  private function _parse_xml( $file ) {
    
    $data = simplexml_load_file( $file );

    // assign each attribute to a variable
    // (extract won't work on an iterable object)
    foreach( $data->attributes() as $attribute => $value ) $$attribute = $value;

    // returns false if there are no media files in the directory
    if ( $count == 0 ) return false;
    
    $playlist = array();
    
    foreach( $data as $song ) {
      
      $playlist[] = array(
        'title'                      => current( $song->title ),
        current( $song->fileformat ) => $this->audio_path.DIRECTORY_SEPARATOR.$genre.DIRECTORY_SEPARATOR.current( $song->filename ),
        'artist'                     => current( $song->artist ),
      );
      
    }
    
    return $playlist;
    
  }

  /**
   * creates and saves a playist XML file
   *
   * @param string $genre     the subdirectory from which the XML data is collected and 
   *                           into which the XML file is saved
   * @param string $filename  full path to a file to create, if false, returns XML object
   *
   * @return mixed object, null or false
   */
  private function _make_xml( $genre, $filename = false ) {
    
    if ( ! is_dir( $this->audio_dir.DIRECTORY_SEPARATOR.$genre ) ) return false;
    
    $genre_dir = $this->audio_dir.DIRECTORY_SEPARATOR.$genre;
    
    $playlist = new SimpleXMLElement("<playlist></playlist>");
    $playlist->addAttribute( 'genre', $genre );
    $playlist->addAttribute( 'path', $genre_dir );

    $count = 0;
    
    foreach ( scandir( $genre_dir ) as $item ) {
      
      $file = $this->_get_file_name( $item, $genre_dir );
      
      // skip to the next item if this one is not a proper file
      if ( false === $file ) continue;
      
      $tags = $this->_get_tags( $genre_dir.DIRECTORY_SEPARATOR.$file );
    
      // error_log( __METHOD__.' '.$genre_dir.DIRECTORY_SEPARATOR.$file.' tags:'.print_r( $tags, true ));
      
      // if the file doesn't parse as a media type, go to the next one
      if ( empty( $tags['fileformat'] ) ) continue;
      
      $song = $playlist->addChild( 'song' );
      
      foreach ( $tags as $tag => $value ) {
        
        $song->addChild( $tag, $value );
        
      }

      $count++;
      
    }

    // add the "number of songs in this genre" attribute
    $playlist->addAttribute( 'count', $count );
    
    //error_log( __METHOD__.' playlist:'.print_r( $playlist, true ));
    
    // save the XML file
    if ( false !== $filename ) $playlist->asXML( $filename );

    // or return the XML object
    else return $playlist;
    
  }
  
  /*******************
   * AUDIO FILE TAG METHODS
   */
   
  /**
   * gets the tags for an audio file
   *
   * @param string $file the file path
   * @return associative array of song data
   */
  private function _get_tags( $file ) {
    
    if ( ! is_file( $file ) ) return false;
    
    $tags = array();
    
    $ID3 = new getID3;
    $ID3_info = $ID3->analyze( $file );
    getid3_lib::CopyTagsToComments( $ID3_info );
    
    //error_log( __METHOD__.' file info:'.print_r( $ID3_info,true ));
    
    $tags['fileformat'] = $ID3_info['fileformat'];
    $tags['artist'] = $ID3_info['comments_html']['artist'][0];
    $tags['title'] = $ID3_info['comments_html']['title'][0];
    $tags['filename'] = $ID3_info['filename'];
    
    return $tags;
    
  }
  
  /*******************
   * UTILITY METHODS
   */
   
  /**
   * get a valid directory or file name
   *
   * @param string  $item  the name of a directory item
   * @param bool    $check if true (default) returns a directory name
   *                       if false returns a file name
   * @param string  $dir   the directory to look in to check item
   */
  private function _get_directory_name( $item, $check = true, $dir = '' ) {
    
    if ( empty( $dir ) ) $dir = $this->audio_dir;

    // weeds out hidden files, system files, traversals and the XML file
    if ( preg_match('|^[.~_]|', $item) > 0 || in_array($item, array( self::PLAYLIST_FILE ) ) ) return false;
    
    return ( $check === is_dir( $dir.DIRECTORY_SEPARATOR.$item ) ) ? $item : false;
    
  }
  
  /**
   * returns a filename or false
   *
   * @param string $item the directory item string
   */
  private function _get_file_name( $item, $dir = '' ) {
    
    return $this->_get_directory_name( $item, false, $dir );
    
  }

  /**
   * makes a title out of a directory name or other string with dashes or underscores
   *
   * @param string $dir
   * @return string
   */
  private function _make_title( $string ) {
    
    return ucwords( str_replace( array( '-','_' ), ' ', $string ) );
    
  }
  
  /**
   * finds the web root url
   *
   * not a well-tested function; this is difficult to implement universally.
   * This assumes the class file is in a directory below root, and is based on
   * the idea that removing the filename and containing directory(ies) from the
   * full path leaves the web root.
   *
   * @param int $depth the number of directories down this class file is from
   *                   web root
   */
  private function _get_root_URL( $depth = 1 ) {
    
    $parts = explode( DIRECTORY_SEPARATOR, $_SERVER['PHP_SELF'] );
    array_pop( $parts ); // the filename
    for ( $x = 1; $x <= $depth; $x++ ) array_pop( $parts ); // the subdirectories
    return 'http://'.$_SERVER['HTTP_HOST'].dirname($_SERVER['SCRIPT_NAME']).DIRECTORY_SEPARATOR.implode( DIRECTORY_SEPARATOR, $parts );

  }
}